// Copyright 2022 Huawei Cloud Computing Technology Co., Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "CaeSocketAgent.h"

#include <utility>

#include "cJSON.h"

#include "../cae_agent/CaeAgent.h"
#include "../cae_common/CaeCommon.h"
#include "CaeHttpsJob.h"
#include "CaeParamStorage.h"
#include "../cae_common/CaeMsgCode.h"
#include "CaeMediaConfig.h"

static const uint32_t MAX_INTERVAL = 1000;

void CaeSocketAgent::LockClientSocket()
{
    pthread_mutex_lock(&m_clientSocketLock);
}

void CaeSocketAgent::UnlockClientSocket()
{
    pthread_mutex_unlock(&m_clientSocketLock);
}

bool CaeSocketAgent::IsSocketValid()
{
    return m_sendFlag && m_clientSocket != nullptr;
}

CaeSocketAgent::CaeSocketAgent()
{
    this->m_clientSocket = nullptr;
    this->m_clientType = CLIENT_TYPE;
    this->m_clientDataHandleThread = nullptr;
    this->m_pauseTimer = nullptr;
    this->m_notouchTimer = nullptr;
    this->m_cmdPipeDataHandleThread = nullptr;
    this->m_trailTimer = nullptr;
    pthread_mutex_init(&m_lock, nullptr);
    pthread_mutex_init(&m_clientSocketLock, nullptr);
    pthread_mutex_init(&m_cmdQueueMutex, nullptr);
    this->m_notice = nullptr;
    DisableSend();
}

CaeSocketAgent::~CaeSocketAgent()
{
    LOGI("CaeSocketAgent desctruct, %s", GetSocketConnectionId().c_str());
    pthread_mutex_destroy(&m_lock);
    pthread_mutex_destroy(&m_clientSocketLock);
    pthread_mutex_destroy(&m_cmdQueueMutex);
    if (m_clientSocket != nullptr) {
        delete m_clientSocket;
        m_clientSocket = nullptr;
    }
    if (m_notice != nullptr) {
        ((CaeClientNotice *)m_notice)->Stop();
        delete m_notice;
        m_notice = nullptr;
    }
    if (m_pauseTimer != nullptr) {
        delete m_pauseTimer;
        m_pauseTimer = nullptr;
    }
    if (m_trailTimer != nullptr) {
        delete m_trailTimer;
        m_trailTimer = nullptr;
    }
    if (m_notouchTimer != nullptr) {
        delete m_notouchTimer;
        m_notouchTimer = nullptr;
    }

    DestroyThreads();

    while (!m_cmdQueue.empty()) {
        auto tempData = m_cmdQueue.front();
        if (nullptr != tempData) {
            free((void *) tempData->GetBuffer());
            m_cmdQueue.pop();
            delete tempData;
            tempData = nullptr;
        }
    }

    LOGI("CaeSocketAgent desctruct done.");
}

int CaeSocketAgent::Send(void *pkt, size_t size)
{
    int ret = -1;
    if (IsSocketValid() && m_clientSocket->GetStatus() != SOCKET_STATUS_ERROR) {
        ret = m_clientSocket->Send(pkt, size);
        if (ret < 0) {
            LOGE("Failed to send pkt.");
        }
    }
    return ret;
}

int CaeSocketAgent::Recv(void *pPkt, size_t size)
{
    int ret = -1;
    if (IsSocketValid() && m_clientSocket->GetStatus() != SOCKET_STATUS_ERROR) {
        ret = m_clientSocket->Recv(pPkt, size);
        if (ret < 0) {
            LOGD("No data to receive.");
        }
    }
    return ret;
}

void CaeSocketAgent::CreateClientNotice()
{
    if (m_notice != nullptr) {
        ((CaeClientNotice *)m_notice)->SetDisconnectCallback(nullptr);
        ((CaeClientNotice *)m_notice)->SetDisconnectTimeoutCallback(nullptr);
        ((CaeClientNotice *)m_notice)->Stop();
        delete m_notice;
        m_notice = nullptr;
    }
    m_notice = (void *)new(std::nothrow) CaeClientNotice(m_clientSocket->GetConnectionId());
    if (m_notice == nullptr) {
        LOGE("Create m_notice failed.");
    }
}

void CaeSocketAgent::SetClientSocket(CaeSocket *socket)
{
    LOGI("Start setting client sockets.");
    pthread_mutex_lock(&m_lock);
    if (socket == nullptr) {
        pthread_mutex_unlock(&m_lock);
        LOGI("End Setting Client Sockets.");
        return;
    }

    m_clientSocket = socket;
    m_clientSocket->SetSocketOption(SOCKET_OPTION_BITSET_NODELAY | SOCKET_OPTION_BITSET_QUICKACK);
    m_clientSocket->SetSocketTimeout(1, 0);

    CreateClientNotice();

    if (m_socketAgentState == CAE::INIT) {
        m_clientSocket->SetEventNotice((CaeEventNotice *) m_notice);
    } else if (m_socketAgentState == CAE::WAIT_RECONNECT || m_socketAgentState == CAE::PAUSED_DISCONNECT) {
        m_clientSocket->SetEventNotice((CaeEventNotice *) m_notice);
        RestartErrorTimer();
    } else {
        LOGD("Nothing todo.");
    }

    LOGD("End to set new socket:%x", socket->GetFd());
    EnableSend();

    std::weak_ptr<CaeSocketAgent> weakPtr = GetSelf();
    if (m_clientDataHandleThread == nullptr) {
        m_clientDataHandleThread = new(std::nothrow) CaeClientDataHandleThread(weakPtr);
        if (m_clientDataHandleThread == nullptr) {
            LOGE("Failed to create client data handle thread.");
        } else {
            m_clientDataHandleThread->Start();
            LOGI("Start client data handle thread. ");
        }
    }

    if (m_cmdPipeDataHandleThread == nullptr) {
        m_cmdPipeDataHandleThread =
                new(std::nothrow) CaeControlCmdHandleThread(weakPtr);
        if (m_cmdPipeDataHandleThread == nullptr) {
            LOGE("Failed to create cmd pipe data handle thread.");
        } else {
            m_cmdPipeDataHandleThread->Start();
            LOGI("Start cmd pipe data handle thread. ");
        }
    }

    pthread_mutex_unlock(&m_lock);
    LOGI("End Setting Client Sockets.");
}

ServerType CaeSocketAgent::GetServerType()
{
    return m_clientSocket->GetServerType();
}

string CaeSocketAgent::GetClientPlatformType()
{
    if (m_cmdPipeDataHandleThread == nullptr) {
        return "";
    }
    return m_cmdPipeDataHandleThread->GetParamStorage()->GetClientPlatformType();
}

string CaeSocketAgent::GetSdkVersion()
{
    if (m_cmdPipeDataHandleThread == nullptr) {
        return "";
    }
    return m_cmdPipeDataHandleThread->GetParamStorage()->GetSdkVersion();
}

void CaeSocketAgent::Reset()
{
    LOGI("Start resetting %s.", m_clientSocket->GetConnectionId().c_str());
    pthread_mutex_lock(&m_lock);

    this->DestroyThreads();

    if (m_clientSocket != nullptr) {
        m_clientSocket->SetEventNotice(nullptr);
        m_clientSocket->CloseSocket();
        delete m_clientSocket;
        m_clientSocket = nullptr;
    }

    if (nullptr != m_notice) {
        auto *notice = (CaeClientNotice *) m_notice;
        notice->Stop();
        delete notice;
        m_notice = nullptr;
    }

    StopPauseTimer();
    StopTrailTimer();
    StopNotouchTimer();

    pthread_mutex_unlock(&m_lock);
    LOGI("End resetting.");
}

void CaeSocketAgent::DisableSend()
{
    LockClientSocket();
    this->m_sendFlag = false;
    UnlockClientSocket();
}

void CaeSocketAgent::EnableSend()
{
    LockClientSocket();
    this->m_sendFlag = true;
    UnlockClientSocket();
}

void CaeSocketAgent::RestartErrorTimer()
{
    if (m_notice != nullptr) {
        auto *notice = (CaeClientNotice *)m_notice;
        notice->RestartErrorTimer();
    }
}

void CaeSocketAgent::CloseOldClient()
{
    LOGI("Start closing old client.");
    pthread_mutex_lock(&m_lock);
    Result closeRet = CaeEngineControl::GetInstance().CloseMediaStream();
    if (closeRet != SUCCESS) {
        LOGE("Failed to close media stream.");
    }
    CaeEngineControl::GetInstance().CloseVirtualDevices();
    DestroyThreads();
    CloseOldSocket();
    CaeAgent::GetInstance().ClearPipesData();
    if (m_clientSocket != nullptr) {
        delete m_clientSocket;
        m_clientSocket = nullptr;
    }

    if (CaeAgent::GetInstance().GetStatus() == CaeAgentStatus::CAE_AGENT_PAUSE) {
        StartPauseTimer();
    }
    pthread_mutex_unlock(&m_lock);
    LOGI("End closing old client.");
}

void CaeSocketAgent::PauseClient()
{
    LOGI("Start pausing client.");
    pthread_mutex_lock(&m_lock);
    Result closeRet = CaeEngineControl::GetInstance().CloseMediaStream();
    if (closeRet != SUCCESS) {
        LOGE("Failed to closing media stream.");
    }
    CaeEngineControl::GetInstance().CloseVirtualDevices();
    StartPauseTimer();
    pthread_mutex_unlock(&m_lock);
    LOGI("End pausing client.");
}

void CaeSocketAgent::CloseOldSocket()
{
    if (m_clientSocket != nullptr) {
        m_clientSocket->SetEventNotice(nullptr);
        m_clientSocket->CloseSocket();
    }
}

void CaeSocketAgent::UpdateErrorTimerTime()
{
    if (m_socketAgentState == CAE::WAIT_RECONNECT || m_socketAgentState == CAE::PAUSED_DISCONNECT) {
        return;
    }
    auto *notice = (CaeClientNotice *)m_notice;
    if (notice == nullptr || notice->m_errorTimer == nullptr) {
        return;
    }
    gettimeofday(&(notice->m_errorTimer->m_lasttime), nullptr);
    if (notice->m_errorTimer->m_disconnectTimes >= 1 &&
        notice->m_errorTimer->m_disconnectTimes < notice->m_errorTimer->GetDisconnectTimesMax()) {
        notice->m_errorTimer->Restart();
    }
}

void CaeSocketAgent::UpdateNoTouchTimer()
{
    if (m_notouchTimer != nullptr) {
        this->m_notouchTimer->Restart();
    }
}

bool CaeSocketAgent::StartNotouchTimer()
{
    StopNotouchTimer();
    auto touchTimeout = static_cast<uint32_t>(atoi(m_cmdPipeDataHandleThread->GetParamStorage()->GetTouchTimeout().c_str()));
    if (touchTimeout > 0 && m_clientSocket != nullptr) {
        LOGD("Before starting, CaeTimerManage's size is %d.",
             CaeTimerManage::GetInstance().m_tVec.size());
        uint32_t interval = CaeConfigManage::GetOutTimes() *
                            CaeConfigManage::GetBaseTime();
        if (interval <= 0 || interval > MAX_INTERVAL) {
            LOGE("Failed to start no touch timer, interval is %d. interval is invalid!", interval);
            interval = 1000;
        }
        uint32_t timeout =
                static_cast<uint32_t>(atoi(m_cmdPipeDataHandleThread->GetParamStorage()->GetTouchTimeout().c_str()));
        m_notouchTimer = new (std::nothrow) CaeNotouchTimer(interval, timeout, m_clientSocket->GetConnectionId());
        if (m_notouchTimer == nullptr) {
            LOGE("Failed to start no touch timer, failed to create no touch timer .");
            return false;
        }
        m_notouchTimer->Start();
        CaeTimerManage::GetInstance().AddTimer(m_notouchTimer);
        LOGD("After startup, CaeTimerManage's size is %d", CaeTimerManage::GetInstance().m_tVec.size());
    }
    return true;
}

void CaeSocketAgent::StopNotouchTimer()
{
    if (m_notouchTimer != nullptr) {
        LOGI("Start stop NotouchTimer.");
        CaeTimerManage::GetInstance().DeleteTimer((CaeTimer *)m_notouchTimer);
        m_notouchTimer = nullptr;
        LOGI("End stop NotouchTimer.");
    }
    CaeClientNotifyTool::GetInstance().ResetTouchTimeout();
}

void CaeSocketAgent::StartPauseTimer()
{
    LOGD("Before starting, CaeTimerManage's size is %d.", CaeTimerManage::GetInstance().m_tVec.size());
    if (m_pauseTimer != nullptr) {
        return;
    }

    if (m_clientSocket == nullptr) {
        return;
    }

    uint32_t timeoutSec =
            static_cast<uint32_t>(atoi(m_cmdPipeDataHandleThread->GetParamStorage()->GetBackgroundTimeout().c_str()));
    m_pauseTimer = new CaeTcpClientPauseTimer(CaeConfigManage::GetOutTimes() *
                                              CaeConfigManage::GetBaseTime(), timeoutSec,
                                              m_clientSocket->GetConnectionId());
    m_pauseTimer->Start();
    CaeTimerManage::GetInstance().AddTimer((CaeTimer *) m_pauseTimer);
    LOGD("after StartPauseTimer, CaeTimerManage'size is %d.", CaeTimerManage::GetInstance().m_tVec.size());
}

void CaeSocketAgent::StopPauseTimer()
{
    if (m_pauseTimer != nullptr) {
        LOGI("Start stop PauseTimer.");
        CaeTimerManage::GetInstance().DeleteTimer((CaeTimer *)m_pauseTimer);
        m_pauseTimer = nullptr;
        LOGI("End stop PauseTimer.");
    }
}

bool CaeSocketAgent::StartTrailTimer()
{
    if (m_trailTimer != nullptr) {
        LOGI("Start stopping the old trail timer.");
        CaeTimerManage::GetInstance().DeleteTimer((CaeTimer *)m_trailTimer);
        m_trailTimer = nullptr;
        LOGI("End to stop the old trail timer.");
    }

    uint32_t trialTime =
            static_cast<uint32_t>(atoi(m_cmdPipeDataHandleThread->GetParamStorage()->GetAvailablePlayTime().c_str()));
    LOGI("Start trail timer = %d", trialTime);
    if (trialTime > 0 && m_clientSocket != nullptr) {
        m_trailTimer = new (std::nothrow) CaeTrialTimeoutTimer(trialTime, m_clientSocket->GetConnectionId());
        if (m_trailTimer == nullptr) {
            LOGE("Failed to start trail timer, failed to create trail timer.");
            return false;
        }
        m_trailTimer->Start();
        CaeTimerManage::GetInstance().AddTimer((CaeTimer *) m_trailTimer);
    }
    return true;
}

void CaeSocketAgent::StopTrailTimer()
{
    if (m_trailTimer != nullptr) {
        LOGI("Stop trail timer.");
        CaeTimerManage::GetInstance().DeleteTimer((CaeTimer *)m_trailTimer);
        m_trailTimer = nullptr;
    }
    CaeClientNotifyTool::GetInstance().ResetTrailTimeout();
}

void CaeSocketAgent::DestroyThreads() noexcept
{
    if (m_clientDataHandleThread != nullptr) {
        m_clientDataHandleThread->Stop();
        while (m_clientDataHandleThread->m_threadStatus != CAE_THREAD_STATUS_EXIT) {
            usleep(10);
        }
        delete m_clientDataHandleThread;
        m_clientDataHandleThread = nullptr;
    }
    if (m_cmdPipeDataHandleThread != nullptr) {
        m_cmdPipeDataHandleThread->Stop();
        while (m_cmdPipeDataHandleThread->m_threadStatus != CAE_THREAD_STATUS_EXIT) {
            usleep(10);
        }
        delete m_cmdPipeDataHandleThread;
        m_cmdPipeDataHandleThread = nullptr;
    }
}

string CaeSocketAgent::GetSocketConnectionId()
{
    return m_connectionId;
}

void CaeSocketAgent::SetSocketConnectionId(string connectionId)
{
    m_connectionId = std::move(connectionId);
}

string CaeSocketAgent::GetClientIp()
{
    return m_clientIp;
}

void CaeSocketAgent::SetClientIp(string clientIp)
{
    m_clientIp = std::move(clientIp);
}

string CaeSocketAgent::GetClientPort()
{
    return m_clientPort;
}

void CaeSocketAgent::SetClientPort(string clientPort)
{
    m_clientPort = std::move(clientPort);
}

void CaeSocketAgent::AddCmdPkt(uint8_t *data, size_t size)
{
    uint8_t *toAppendData = nullptr;
    size_t toAppendDataSize = size;

    toAppendData = static_cast<uint8_t *>(malloc(toAppendDataSize));
    if (toAppendData == nullptr) {
        LOGE("Failed to create toAppendData, type:%d.", CAE_MSG_CONTROL);
        return;
    }
    if (memset_s(toAppendData, toAppendDataSize, '\0', toAppendDataSize) != EOK) {
        LOGE("Failed to memset toAppendData, type: %d.", CAE_MSG_CONTROL);
        free(toAppendData);
        toAppendData = nullptr;
        return;
    }

    if (memcpy_s(toAppendData, toAppendDataSize, data, toAppendDataSize) != EOK) {
        LOGE("Failed to memcpy data, type: %d.", CAE_MSG_CONTROL);
        free(toAppendData);
        toAppendData = nullptr;
        return;
    }

    auto *tempData = new(std::nothrow) CaePipeData();
    if (tempData == nullptr) {
        LOGE("Failed to write cmd data to queue, size:%d.", size);
        return;
    }
    bool initState = tempData->Init(toAppendData, toAppendDataSize, CAE_MSG_CONTROL);
    if (!initState) {
        delete tempData;
        tempData = nullptr;
        return;
    }

    pthread_mutex_lock(&m_cmdQueueMutex);
    m_cmdQueue.push(tempData);
    pthread_mutex_unlock(&m_cmdQueueMutex);

}

CaePipeData *CaeSocketAgent::GetCmdPkt()
{
    CaePipeData *pipeData;
    pipeData = nullptr;

    pthread_mutex_lock(&m_cmdQueueMutex);
    if (!m_cmdQueue.empty()) {
        pipeData = m_cmdQueue.front();
        m_cmdQueue.pop();
        pthread_mutex_unlock(&m_cmdQueueMutex);
    } else {
        pthread_mutex_unlock(&m_cmdQueueMutex);
        return pipeData;
    }
    return pipeData;
}

shared_ptr<CaeParamStorage> CaeSocketAgent::GetConnectionParams()
{
    if (m_cmdPipeDataHandleThread == nullptr) {
        return nullptr;
    }
    return m_cmdPipeDataHandleThread->GetParamStorage();
}

void CaeSocketAgent::SetConnectionParams(shared_ptr<CaeParamStorage> &paramStorage)
{
    m_cmdPipeDataHandleThread->SetParamStorage(paramStorage);
}

void CaeSocketAgent::SetIsVerifyComplete(bool mIsVerifyComplete)
{
    m_isVerifyComplete = mIsVerifyComplete;
}

bool CaeSocketAgent::IsVerifyComplete() const
{
    return m_isVerifyComplete;
}

void CaeSocketAgent::SetVerifyCallback(const VerifyRetCallback & callback)
{
    if (m_cmdPipeDataHandleThread != nullptr) {
        m_cmdPipeDataHandleThread->SetVerifyCallback(callback);
    }
}

void CaeSocketAgent::SetActionCallback(const ActionCallback & callback)
{
    if (m_cmdPipeDataHandleThread != nullptr) {
        m_cmdPipeDataHandleThread->SetActionCallback(callback);
    }
}

void CaeSocketAgent::SetSocketAgentState(CAE::CaeSocketAgentState socketAgentState)
{
    m_socketAgentState = socketAgentState;
    switch (socketAgentState) {
        case CAE::RUNNING:
            CaeAgent::GetInstance().AddVerifiedConnectionId(m_clientIp, m_clientPort);
            break;
        case CAE::WAIT_CLOSE:
        case CAE::DEINIT:
            CaeAgent::GetInstance().DeleteVerifiedConnectionId(m_clientIp, m_clientPort);
            break;
        default:
            break;
    }
}

CAE::CaeSocketAgentState CaeSocketAgent::GetSocketAgentState() const
{
    return m_socketAgentState;
}

string CaeSocketAgent::GetSocketAgentStateString() const
{
    switch (m_socketAgentState) {
        ENUM_CHIP_TYPE_CASE(CAE::INIT)
        ENUM_CHIP_TYPE_CASE(CAE::VERIFYING)
        ENUM_CHIP_TYPE_CASE(CAE::VERIFYED)
        ENUM_CHIP_TYPE_CASE(CAE::RUNNING)
        ENUM_CHIP_TYPE_CASE(CAE::PAUSED)
        ENUM_CHIP_TYPE_CASE(CAE::PAUSED_DISCONNECT)
        ENUM_CHIP_TYPE_CASE(CAE::WAIT_RECONNECT)
        ENUM_CHIP_TYPE_CASE(CAE::WAIT_CLOSE)
        ENUM_CHIP_TYPE_CASE(CAE::DEINIT)
        default:
            return "UNKOWN_STATE";
    }
}

void CaeSocketAgent::SetDisconnectCallback(const TimeoutHook & callback)
{
    if (m_notice != nullptr) {
        auto *notice = (CaeClientNotice *)m_notice;
        notice->SetDisconnectCallback(callback);
    }
}

void CaeSocketAgent::SetDisconnectTimeoutCallback(const TimeoutHook & callback)
{
    if (m_notice != nullptr) {
        auto *notice = (CaeClientNotice *)m_notice;
        notice->SetDisconnectTimeoutCallback(callback);
    }
}

void CaeSocketAgent::SetNoTouchTimeoutCallback(const TimeoutHook & callback)
{
    if (m_notouchTimer != nullptr) {
        m_notouchTimer->SetTimeoutHook(callback);
    }
}

void CaeSocketAgent::SetTrialTimeoutCallback(const TimeoutHook & callback)
{
    if (m_trailTimer != nullptr) {
        m_trailTimer->SetTimeoutHook(callback);
    }
}

void CaeSocketAgent::SetPauseTimeoutCallback(const TimeoutHook & callback)
{
    if (m_pauseTimer != nullptr) {
        m_pauseTimer->SetTimeoutHook(callback);
    }
}
